package com.github.captain_miao.android.bluetoothletutorial; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattDescriptor; import android.bluetooth.BluetoothGattService; import android.content.Intent; import android.os.Build; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v4.content.ContextCompat; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.Toolbar; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; import android.view.View; import android.widget.Button; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.EditText; import android.widget.TextView; import com.afollestad.materialdialogs.DialogAction; import com.afollestad.materialdialogs.MaterialDialog; import com.afollestad.materialdialogs.internal.MDTintHelper; import com.afollestad.materialdialogs.util.DialogUtils; import com.bignerdranch.expandablerecyclerview.Adapter.ExpandableRecyclerAdapter; import com.bignerdranch.expandablerecyclerview.Model.ParentListItem; import com.github.captain_miao.android.ble.BleCallback; import com.github.captain_miao.android.ble.BluetoothHelper; import com.github.captain_miao.android.ble.ConnectCallback; import com.github.captain_miao.android.ble.constant.BleConnectState; import com.github.captain_miao.android.ble.constant.ConnectError; import com.github.captain_miao.android.ble.utils.BleUtils; import com.github.captain_miao.android.ble.utils.HexUtil; import com.github.captain_miao.android.bluetoothletutorial.app.AppLog; import com.github.captain_miao.android.bluetoothletutorial.ble.AppBluetoothHelper; import com.github.captain_miao.android.bluetoothletutorial.constant.AppConstants; import com.github.captain_miao.android.bluetoothletutorial.expandablerecyclerview.VerticalChildObject; import com.github.captain_miao.android.bluetoothletutorial.expandablerecyclerview.VerticalExpandableAdapter; import com.github.captain_miao.android.bluetoothletutorial.expandablerecyclerview.VerticalParentObject; import com.github.captain_miao.android.bluetoothletutorial.fragment.BottomSheetLogView; import com.github.captain_miao.android.bluetoothletutorial.model.BleCommandInfo; import com.github.captain_miao.android.bluetoothletutorial.model.BleDevice; import com.github.captain_miao.android.supportsdk.BaseActivity; import com.github.captain_miao.android.supportsdk.app.AppToast; import com.github.captain_miao.android.supportsdk.utils.DateUtils; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.UUID; /** * Created by on 15/8/3. */ public class BleDeviceActivity extends BaseActivity implements View.OnClickListener, BluetoothHelper.OnBindListener, ExpandableRecyclerAdapter.ExpandCollapseListener { private static final String TAG = BleDeviceActivity.class.getSimpleName(); private TextView mTVConnectionState; private TextView mDataField; private String mDeviceName; private String mDeviceAddress; private VerticalExpandableAdapter mExpandableAdapter; private RecyclerView mRecyclerView; private Map<String,BluetoothGattCharacteristic> mCharacteristicsMap = new HashMap<>(); private BleDevice mDevice; private AppBluetoothHelper mBleHelper; private MaterialDialog dialog; private TextView mDataCharacteristic; @Override public void init(Bundle savedInstanceState) { setContentView(R.layout.act_device); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); if(getSupportActionBar() != null) { getSupportActionBar().setDisplayHomeAsUpEnabled(true); } mDevice = (BleDevice) getIntent().getSerializableExtra(AppConstants.KEY_BLE_DEVICE); mDeviceName = mDevice.name; mDeviceAddress = mDevice.address; if (TextUtils.isEmpty(mDeviceAddress)) { finish(); return; } setTitle(mDeviceName); initView(); showProgressDialog(); mBleHelper = new AppBluetoothHelper(this); mBleHelper.setBleCallback(mBleCallback); mBleHelper.bindService(this); } private void initView() { //findViewById(R.id.btn_check_device).setOnClickListener(this); // Sets up UI references. ((TextView) findViewById(R.id.device_address)).setText(mDeviceAddress); mRecyclerView = (RecyclerView) findViewById(R.id.linear_recycler_view); mRecyclerView.setLayoutManager(new LinearLayoutManager(this)); mTVConnectionState = (TextView) findViewById(R.id.connection_state); mDataField = (TextView) findViewById(R.id.data_value); } @Override protected void onPause() { super.onPause(); //mBleHelper.release(); } @Override protected void onDestroy() { super.onDestroy(); mBleHelper.release(); } @Override public void onClick(View v) { switch (v.getId()) { } } //显示 10行数据 Queue<String> mDataQueue = new LinkedList<String>(); private BleCallback mBleCallback = new BleCallback() { @Override public void onFailed(String msg) { appendLog("onFailed " + msg); } @Override public void onDescriptorWrite(UUID uuid, int status) { AppLog.i(TAG, "onDescriptorWrite: " + BleUtils.getGattStatus(status)); appendLog("onDescWrite: " + BleUtils.getGattStatus(status)); } @Override public void onCharacteristicRead(UUID uuid, byte[] data) { String values = HexUtil.encodeHexStr(data); AppLog.i(TAG, "onCharacteristicRead: " + values); appendLog("onCharRead: " + values); if (dialog != null && dialog.isShowing()) { mDataCharacteristic.setText(values); } } @Override public void onCharacteristicNotification(UUID uuid, byte[] data) { String values = HexUtil.encodeHexStr(data); AppLog.i(TAG, "onCharacteristicNotification: " + values); appendLog("onChaNotify: " + values); mDataQueue.add(values); int size = mDataQueue.size(); while (size > 10){ mDataQueue.poll(); size--; } mDataField.setText(""); for(String v : mDataQueue){ mDataField.append(v); mDataField.append("\n"); } if(sb != null && sb.length() < 100 * 1024) { sb.append(values).append("\n"); } else { mDataField.append(getString(R.string.app_tips_notify_data_too_long)); } } @Override public void onCharacteristicWrite(UUID uuid, int status) { AppLog.i(TAG, "onCharacteristicWrite: " + BleUtils.getGattStatus(status)); appendLog("onCharWrite: " + BleUtils.getGattStatus(status)); } @Override public void onConnectionStateChange(int status, int newStatus) { BleConnectState connectState = BleConnectState.getBleConnectState(newStatus); mTVConnectionState.setText(AppBluetoothHelper.getConnectStateForShow(BleDeviceActivity.this, connectState.getCode())); appendLog("stateChange: " + AppBluetoothHelper.getConnectStateForShow(BleDeviceActivity.this, connectState.getCode())); } @Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { //服务发现成功 if (gatt != null && status == BluetoothGatt.GATT_SUCCESS) { displayGattServices(gatt.getServices()); appendLog("Discovered success"); } else { AppToast.show(BleDeviceActivity.this, R.string.app_tips_discover_services_fail); appendLog("Discovered fail"); } } }; // Demonstrates how to iterate through the supported GATT Services/Characteristics. // In this sample, we populate the data structure that is bound to the ExpandableListView // on the UI. private void displayGattServices(List<BluetoothGattService> gattServices) { if (gattServices == null) return; mExpandableAdapter = new VerticalExpandableAdapter(this, getAdapterData(gattServices)) { @Override protected void onClickCharacteristic(final String serviceUUID, final String characteristicUUID) { dialog = new MaterialDialog.Builder(BleDeviceActivity.this) .title(R.string.app_tips_write_read_data) .customView(R.layout.dialog_customview, true) .positiveText(R.string.label_ok) .cancelable(true) .negativeText(R.string.label_cancel) .show(); View dialogView = dialog.getCustomView(); if(dialogView != null) { final BluetoothGattCharacteristic characteristic = mCharacteristicsMap.get(characteristicUUID); final int charaProp = characteristic.getProperties(); //发送数据 mDataCharacteristic = (TextView) dialogView.findViewById(R.id.tv_read_characteristic_data); final EditText hexEdit = (EditText) dialogView.findViewById(R.id.write_data_value); if (hexEdit != null) { hexEdit.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { final int length = s.toString().length(); invalidateInputMinMaxIndicator(hexEdit, length); } @Override public void afterTextChanged(Editable s) { } }); View positive = dialog.getActionButton(DialogAction.POSITIVE); positive.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { String hex = hexEdit.getText().toString(); if (!TextUtils.isEmpty(hex)) { if (hex.length() > 20) { AppToast.showCenter(BleDeviceActivity.this, R.string.app_tips_bluetooth_data_max_len); } else if (hex.length() % 2 == 0) { mBleHelper.writeCharacteristic(UUID.fromString(serviceUUID), UUID.fromString(characteristicUUID), HexUtil.hexStringToByteArray(hex)); dialog.dismiss(); } else { AppToast.showCenter(BleDeviceActivity.this, R.string.app_tips_data_must_be_even); } } else { AppToast.showCenter(BleDeviceActivity.this, R.string.app_tips_command_empty); } } }); final Button loadCommand = (Button) dialogView.findViewById(R.id.btn_load_command); loadCommand.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { final BleCommandInfo[] commandInfos = BleCommandInfo.queryAllCommands(); new MaterialDialog.Builder(mContext) .title(R.string.label_command) .items(commandInfos) .itemsCallback(new MaterialDialog.ListCallback() { @Override public void onSelection(MaterialDialog dialog, View view, int which, CharSequence text) { if (!TextUtils.isEmpty(text)) { if (text.length() >= 5) { BleCommandInfo command = commandInfos[which]; hexEdit.setText(command.command); hexEdit.setSelection(command.command.length()); } } } }) .show(); } }); if((charaProp & BluetoothGattCharacteristic.PROPERTY_WRITE) == BluetoothGattCharacteristic.PROPERTY_WRITE) { dialog.findViewById(R.id.write_container_view).setVisibility(View.VISIBLE); } else { dialog.findViewById(R.id.write_container_view).setVisibility(View.GONE); } } //读属性值 final Button readButton = (Button) dialogView.findViewById(R.id.btn_read_characteristic_data); if (readButton != null) { readButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mBleHelper.readFromCharacteristic(UUID.fromString(serviceUUID), UUID.fromString(characteristicUUID)); //dialog.dismiss(); } }); if((charaProp & BluetoothGattCharacteristic.PROPERTY_READ) == BluetoothGattCharacteristic.PROPERTY_READ ) { dialog.findViewById(R.id.read_container_view).setVisibility(View.VISIBLE); } else { dialog.findViewById(R.id.read_container_view).setVisibility(View.GONE); } } //开启notify final CheckBox notifyCheckBox = (CheckBox) dialogView.findViewById(R.id.cb_notify); List<BluetoothGattDescriptor> descriptors = characteristic.getDescriptors(); if (descriptors != null && descriptors.size() > 0) { for (BluetoothGattDescriptor descriptor : descriptors) { if(descriptor.getValue() == BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE || descriptor.getValue() == BluetoothGattDescriptor.ENABLE_INDICATION_VALUE ){ notifyCheckBox.setChecked(true); notifyCheckBox.setText("Enable"); } else { notifyCheckBox.setChecked(false); notifyCheckBox.setText("Disable"); } } } if (notifyCheckBox != null) { notifyCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { List<BluetoothGattDescriptor> descriptors = characteristic.getDescriptors(); if (descriptors != null && descriptors.size() > 0) { for (BluetoothGattDescriptor descriptor : descriptors) { final int properties = characteristic.getProperties(); if ((properties | BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) { mBleHelper.updateCharacteristicNotification(UUID.fromString(serviceUUID), UUID.fromString(characteristicUUID), descriptor.getUuid(), isChecked); } } if (isChecked) { sb = new StringBuffer(); } } dialog.dismiss(); } }); if((charaProp & BluetoothGattCharacteristic.PROPERTY_NOTIFY) == BluetoothGattCharacteristic.PROPERTY_NOTIFY) { dialog.findViewById(R.id.notify_container_view).setVisibility(View.VISIBLE); } else { dialog.findViewById(R.id.notify_container_view).setVisibility(View.GONE); } } } //是否支持读、写、notify } }; // Attach this activity to the Adapter as the ExpandCollapseListener mExpandableAdapter.addExpandCollapseListener(this); mRecyclerView.setAdapter(mExpandableAdapter); //mGattServicesList.setAdapter(gattServiceAdapter); } @Override public void onServiceConnected() { connectDevice(mDeviceAddress); } public void connectDevice(String deviceMac){ mBleHelper.connectDevice(deviceMac, new ConnectCallback() { @Override public void onConnectSuccess() { mBleHelper.mConnCallback = null; dismissProgressDialog(); } @Override public void onConnectFailed(ConnectError error) { dismissProgressDialog(); AppToast.show(BleDeviceActivity.this, R.string.app_tips_connect_fail); } }); } /** * Save the instance state of the adapter to keep expanded/collapsed states when rotating or * pausing the activity. */ @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); mExpandableAdapter.onSaveInstanceState(outState); } /** * Load the expanded/collapsed states of the adapter back into the view when done rotating or * resuming the activity. */ @Override protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); mExpandableAdapter.onRestoreInstanceState(savedInstanceState); } @Override public void onListItemExpanded(int position) { } @Override public void onListItemCollapsed(int position) { } private ArrayList<ParentListItem> getAdapterData(List<BluetoothGattService> gattServices) { String unknownServiceString = getResources().getString(R.string.unknown_service); String unknownCharaString = getResources().getString(R.string.unknown_characteristic); ArrayList<ParentListItem> parentObjectList = new ArrayList<>(); for (BluetoothGattService gattService : gattServices) { String serviceUUID = gattService.getUuid().toString(); ArrayList<Object> childObjectList = new ArrayList<>(); List<BluetoothGattCharacteristic> gattCharacteristics = gattService.getCharacteristics(); for (BluetoothGattCharacteristic gattCharacteristic : gattCharacteristics) { VerticalChildObject verticalChildObject = new VerticalChildObject(); String uuid = gattCharacteristic.getUuid().toString(); mCharacteristicsMap.put(uuid, gattCharacteristic); verticalChildObject.mUUIDText = uuid; verticalChildObject.mNameText = SampleGattAttributes.lookup(uuid, unknownCharaString); verticalChildObject.mPermissionText = getString(R.string.label_lbe_permissions, BleUtils.getPermission(gattCharacteristic.getPermissions())); verticalChildObject.mPropertyText = getString(R.string.label_lbe_properties, BleUtils.getProperties(gattCharacteristic.getProperties())); verticalChildObject.mWriteTypeText = getString(R.string.label_lbe_write_type, BleUtils.getWriteType(gattCharacteristic.getWriteType())); childObjectList.add(verticalChildObject); } VerticalParentObject verticalParentObject = new VerticalParentObject(); verticalParentObject.mNameText = SampleGattAttributes.lookup(serviceUUID, unknownServiceString); verticalParentObject.mUUIDText = serviceUUID; verticalParentObject.setChildObjectList(childObjectList); parentObjectList.add(verticalParentObject); } return parentObjectList; } private StringBuffer sb; public void onShare(View view) { if (sb != null && sb.length() > 0) { Intent sendIntent = new Intent(); sendIntent.setAction(Intent.ACTION_SEND); sendIntent.putExtra(Intent.EXTRA_TEXT, sb.toString()); sendIntent.setType("text/plain"); startActivity(sendIntent); } else { AppToast.show(this, "Ble data is empty"); } } public void onClear(View view) { if(sb != null) { sb = new StringBuffer(); } logInfoList.clear(); initLogHeight(); } //5条空数据 为了可以显示BottomSheetDialog private void initLogHeight(){ logInfoList.add(""); logInfoList.add(""); logInfoList.add(""); logInfoList.add(""); logInfoList.add(""); } protected void invalidateInputMinMaxIndicator(EditText input, int currentLength) { if (input != null) { int materialBlue = ContextCompat.getColor(this, com.afollestad.materialdialogs.R.color.md_material_blue_600); int widgetColor = DialogUtils.resolveColor(this, com.afollestad.materialdialogs.R.attr.colorAccent, materialBlue); if (Build.VERSION.SDK_INT >= 21) { widgetColor = DialogUtils.resolveColor(this, android.R.attr.colorAccent, widgetColor); } final boolean isDisabled = currentLength > 20; final int colorText = isDisabled ? ContextCompat.getColor(this, R.color.red) : -1; final int colorWidget = isDisabled ? ContextCompat.getColor(this, R.color.red) : widgetColor; input.setTextColor(colorText); MDTintHelper.setTint(input, colorWidget); } } List<String> logInfoList = new ArrayList<String>(); BottomSheetLogView logView = null; //蓝牙日志 public void onShowLogView(View view){ if(logInfoList.size() < 5) { initLogHeight(); } logView = BottomSheetLogView.show(this, logInfoList); if(!logView.isShowing()) { logView.show(); } } private void appendLog(String log) { logInfoList.add(DateUtils.formatDateDefault(System.currentTimeMillis()) + log); if (logView != null) { logView.appendLog(DateUtils.formatDateDefault(System.currentTimeMillis()) + log); } } }